﻿using System;
using System.Collections.Generic;

namespace Lukianol
{
    /// <summary>
    /// The dictionary is used to store cached items
    /// </summary>
    /// <typeparam name="TKey">Type of the key</typeparam>
    /// <typeparam name="TValue">Type of the value</typeparam>
    public sealed class CacheDictionary<TKey, TValue>
    {
        private readonly IDictionary<TKey, CachedItem<TValue>> _cache;
        private readonly object _cacheLocker = new object();

        public CacheDictionary() 
        {
            _cache = new Dictionary<TKey, CachedItem<TValue>>();
        }

        public CacheDictionary(int cacheSize)
        {
            _cache = new Dictionary<TKey, CachedItem<TValue>>(cacheSize);
        }
        
        public TValue GetValue(TKey key, Func<TKey, TValue> retrieveFunc, TimeSpan valueExpiresIn)
        {
            return GetValue(key, retrieveFunc, x => DateTime.UtcNow + valueExpiresIn);
        }

        public TValue GetValue(TKey key, Func<TKey, TValue> retrieveFunc, DateTime valueExpiresOnUtc)
        {
            return GetValue(key, retrieveFunc, x => valueExpiresOnUtc);
        }

        public TValue GetValue(TKey key, Func<TKey, TValue> retrieveFunc, Func<TValue, DateTime> retrieveValueExpiresOnUtc)
        {
            CachedItem<TValue> cachedItem = ReadCachedItem(key);

            if (cachedItem != null)
                return cachedItem.Value;

            lock (_cacheLocker)
            {
                cachedItem = ReadCachedItem(key);

                if (cachedItem == null)
                {
                    TValue value = retrieveFunc(key);
                    cachedItem = new CachedItem<TValue>(value, retrieveValueExpiresOnUtc(value));
                    AddCachedItem(key, cachedItem);
                }

                return cachedItem.Value;
            }
        }

        public void ExpireValue(TKey key)
        {
            RemoveCachedItem(key);
        }

        private CachedItem<TValue> ReadCachedItem(TKey key)
        {
            if (_cache.ContainsKey(key))
            {
                CachedItem<TValue> item = _cache[key];

                if (!item.IsExpired)
                    return item;

                RemoveCachedItem(key);
            }

            return null;
        }

        private void RemoveCachedItem(TKey key)
        {
            lock (_cacheLocker)
            {
                if (_cache.ContainsKey(key))
                    _cache.Remove(key);
            }
        }

        private void AddCachedItem(TKey key, CachedItem<TValue> value)
        {
            lock (_cacheLocker)
            {
                if (_cache.ContainsKey(key))
                    _cache[key] = value;
                else
                    _cache.Add(key, value);
            }
        }

        #region Nested type: CachedItem

        private sealed class CachedItem<T>
        {
            public CachedItem(T value, DateTime expiredOnUtc)
            {
                Value = value;
                ExpiredOnUtc = expiredOnUtc;
            }

            public T Value { get; private set; }

            public bool IsExpired
            {
                get { return ExpiredOnUtc <= DateTime.UtcNow; }
            }

            private DateTime ExpiredOnUtc { get; set; }
        }

        #endregion
    }
}